#!/usr/bin/python
# -*- coding: utf-8 -*-

#This file is part of pyLot library.
#
# pyLot is free software: you can redistribute it and/or modify
# it under the terms of the GNU Lesser General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# pyLot is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU Lesser General Public License for more details.
#
# You should have received a copy of the GNU Lesser General Public License
# along with pyLot.  If not, see <http://www.gnu.org/licenses/>.

__author__ = u'Pierre Puiseux, Guillaume Baty'
__copyright__ = u"Copyright 2011-2012 (C) andheo, Université de Pau et des Pays de l'Adour"
__credits__ = [u'Pierre Puiseux', u'Guillaume Baty']
__license__ = "GNU Lesser General Public License"

__date__ = "30 avr. 2010"
__author__ = "INRIA pyLot, UPPA"

__all__ = [u'QProcessHandler',
           'PROCESS_ERRORS']

import sys
import time

from traits.api import implements

from Qt4.QtCore import (QProcess, QMutex, SIGNAL)
from Qt4.QtGui import QMessageBox
from pyLot.core import (CommandGroup, PythonCommand, ManualCommand,
                        ShellCommand, TextCommand, CommonRun,
                        CommonProcessHandler, IProcessHandler)

from _pylot.qtgui.default.impl_qtcontroller import (Thread, ShellRun,
                                                    PythonRun)
from _pylot.qtgui.widgets.gui_process import GuiProcess
from _pylot.qtgui.widgets.gui_processhandler import GuiProcessHandler

PROCESS_ERRORS = {
  QProcess.FailedToStart:(u'Failed to start', u'QProcess.FailedToStart'),
  QProcess.Crashed:(u'Crashed', u'QProcess.Crashed'),
  QProcess.Timedout:(u'Timed out', u'QProcess.Timedout'),
  QProcess.WriteError:(u'Write Error', u'QProcess.WriteError'),
  QProcess.ReadError:(u'Read Error', u'QProcess.ReadError'),
  QProcess.UnknownError:(u'Unknown Error', u'QProcess.UnknownError')
  }

class QProcessHandler(CommonProcessHandler):

  locks = {}
  locks['_command_group_queue'] = QMutex() # edit _command_group_queue
  locks['_running_groups'] = QMutex() # edit _running_groups
  locks['_createProcessTab'] = QMutex() # create a new tab
  locks['StartCommandGroup'] = QMutex() # full command group start

  implements(IProcessHandler)

  def __init__(self, wait=True, max_process=3) :
    CommonProcessHandler.__init__(self, wait=wait, max_process=max_process)
    self.name = u'process_handler'
    self._init()

  def _init (self):
    self.main_process = GuiProcess()
    self.widget_process_handler = GuiProcessHandler(self.main_process)
#      char_formats=self.io.char_formats)

    self.widget_process_handler.connect(self.widget_process_handler,
      SIGNAL('processInBackgroundChanged'),
      self._onProcessInBackgroundChecked)
    self.widget_process_handler.connect(self.widget_process_handler,
      SIGNAL('maxProcessChanged'), self._onMaxProcessChanged)

    self.widget_process_handler.setProcessInBackground(not self.wait)
    self.tab_process = self.widget_process_handler.getTabProcess()

    self._initProcessAttrs()

  def _redirectOutputs (self, value=True):
    if value is True :
      sys.stdout = self._fake_stdout
      sys.stderr = self._fake_stderr
    else :
      sys.stdout = sys.__stdout__
      sys.stderr = sys.__stderr__

  def _set_running_groups (self, value):
    self.locks[u'_running_groups'].lock()
    self.__running_groups = value
    self.locks[u'_running_groups'].unlock()

  def _get_running_groups (self):
    self.locks[u'_running_groups'].lock()
    value = self.__running_groups
    self.locks[u'_running_groups'].unlock()
    return value

  def _setWait (self, value):
    try :
      self.widget_process_handler
    except AttributeError :
      pass
    else :
      self.widget_process_handler.setProcessInBackground(not value)

  def _setMaxProcess (self, value):
    try :
      self.widget_process_handler
    except AttributeError :
      pass
    else :
      self.widget_process_handler.setMaxProcess(value)

  _running_groups = property(fget=_get_running_groups,
                             fset=_set_running_groups)

  def getProcessHandlerWidget(self):
    return self.widget_process_handler

  def _getRunningProcess(self):
    return self._running_groups

  def _getInQueueProcess(self):
    return self._command_group_queue

  @classmethod
  def runTextCommand (cls, cmd):
    print u'runTextCommand'

 ##############################################################################
  # Run process
  ##############################################################################
  def _initProcessAttrs(self):
    self._wait_box_dict = {}
    self._unamed_process = 0
    self._running_groups = []
    self._command_group_queue = []
    self._script_names = []
    self._process = {}

  def _addToQueue (self, command_group, wait):
    assert(issubclass(type(command_group), CommandGroup))
    assert(wait in (True, False))

    self.locks[u'_command_group_queue'].lock()
    self._command_group_queue.append((command_group, wait))
    self.locks[u'_command_group_queue'].unlock()

  def _run (self, command_group, wait):
    run_now = True
    if len(self._running_groups) >= self.max_process :
      run_now = False
    else :
      # got a list of process required by process
      # suppose all required process are finished
      for cg_to_wait in command_group._wait_list : # check if it's True
        if cg_to_wait.state == CommonRun.Finished :
          continue
        else :
          # no, it's False, there is at least one required process not finished
          run_now = False
          break

    if run_now is True :
      # A command group is started
      # code is locked until QProcess starts
      # this ensure that widget is created and process state is the good state, ...
      self.locks[u'StartCommandGroup'].lock()
      # No process to wait, max process not reached
      widget, new_tab = self._createProcessTab(command_group, wait)
      if new_tab :
        # tab is new, so widget is free for a new run
        self._runCommandGroup(command_group, widget, wait)
      else :
        # maybe widget isn't free (a process is running)
        if not widget.running :
          # No running process (finished or no proc), so let's go !
          self._runCommandGroup(command_group, widget, wait)
        else :
          # There is a running process, waiting ...
          self.locks[u'StartCommandGroup'].unlock()
          self._addToQueue(command_group, wait)
    else :
      # process to wait or max reached, waiting ...
      self._addToQueue(command_group, wait)
    return command_group

  def _createProcessTab(self, command_group, wait):
    u"""
    Looks if a new tab must be created (depends on command_group.category and
    wait value) and eventually creates it.
    """
    # If category is defined, we use a tab with "category" as name
    # Process can use an existing tab (if category is defined)
    # so we must ensure no other tab process is created at the same time
    # else it may be possible to have two or more tab with same category.
    self.locks[u'_createProcessTab'].lock()
    new_tab_created = False
    if command_group.category :
      try :
        # if tab yet exists, we use this one
        process_widget = self.widget_process_handler.getProcessWidget(command_group.category)
      except KeyError :
        # else we create tab
        new_tab_created = True
        process_widget = self.widget_process_handler.createTab(command_group.category)
    else :
      if wait is True :
        process_widget = self.main_process
      else :
        self._unamed_process += 1
        new_tab_created = True
        name = u'(%d) %s' % (self._unamed_process, command_group.group_id)
        process_widget = self.widget_process_handler.createTab(name)

    self.locks[u'_createProcessTab'].unlock()
    return process_widget, new_tab_created

  def _runCommandGroup (self, command_group, process_widget, wait):
    u"""
    really run command group.
    Here, max simultaneous process isn't reached
    and all required groups are finished
    """
    cg = command_group
    def write(text, style=None):
      self.logger.write(text, style)
      cg.logger.write(text, style)
    def write_stdout(s):
      self.logger.stdout(s)
      cg.logger.stdout(s)
    def write_stderr(s):
      self.logger.write(text, style)
      cg.logger.stderr(s)

    process_widget.running = True
    # create an iterator containing all command_group commands
    cmds = ((cmd for cmd in command_group.cmds))

    all_ok = True # suppose all requireds command_group finished successfully
    for cg_to_wait in command_group._wait_list : # check if it's True
      if cg_to_wait.return_code != 0 :
        # this command_group has failed
        all_ok = False
        break
      else :
        continue

    if all_ok:
      #self._redirectOutputs(True)
      self._running_groups.append(command_group)

      # *** change tab state
      command_group.exec_time.start()
      # TODO: connect here with showProcessTab/showProcessDock
      self.tab_process.setCurrentIndex(self.tab_process.indexOf(process_widget))

      self.widget_process_handler.changeTabState(process_widget, u'running')

      # *** write start summary
      text, style, level = self.formatTextCmdGroupStarted(command_group)
      write(text, style)
      write(text, style)
      write(u'\n')

      # *** update StatusBar
      #TODO: message status bar
#      self.status_bar.clearMessage()
#      text, style, level = self.formatTextCmdGroupStarted(command_group)
#      self.status_bar.showMessage(text)

      # *** start first command
      self._runNextCommand(command_group, process_widget, cmds, wait)
    else :
      command_group.exec_time.start()
      self.locks[u'StartCommandGroup'].unlock()
      self._forceCommandGroupFail(command_group, process_widget, cmds,
                                  cg_to_wait.return_code)

  def runNextCommand(self):
    sender = self.widget_process_handler.sender()
    try :
      args = self._process.pop(sender)
    except KeyError :
      pass
    else :
      self._runNextCommand(*args)

  def _runNextCommand (self, command_group, process_widget, cmds, wait):
    assert (type(command_group) is CommandGroup)
    assert (wait in (True, False))

    try :
      cmd = cmds.next()
    except StopIteration :
      self.locks[u'StartCommandGroup'].unlock()
      self._commandGroupFinished(command_group, process_widget)
    else :
      cmd_idx = command_group._progress[0]
      text, style, level = self.formatTextCmdExecTime(command_group.cmds[cmd_idx])
#      self.io.write(text, style, level, writers=[process_widget.stdout])

      if cmd_idx > 0 :
        # It's not the first command, we start it only if previous was a success
        previous = command_group.cmds[cmd_idx - 1]
        previous.exec_time.stop()
        if previous.return_code != 0 :
          self._forceCommandGroupFail(command_group, process_widget, cmds,
                                      previous.return_code)
        else :
          # Previous command was a success, continue
          self._runCommand(command_group, process_widget, cmds, cmd, wait)
      else :
        # It's the first command, start it
        self.locks[u'StartCommandGroup'].unlock()
        self._runCommand(command_group, process_widget, cmds, cmd, wait)

  def _forceCommandGroupFail (self, command_group, process_widget, cmds,
                              return_code):
    # force this group to fail
    command_group.cmds[-1].STATE = CommonRun.Finished
    command_group.cmds[-1].RETURN_CODE = return_code

    # force StopIteration at next _runNextCommand call
    cmds.close()

    # send _commandGroupFinished signal
    self._commandGroupFinished(command_group, process_widget)

  def _commandGroupFinished (self, command_group, process_widget):
    u"""
    Display summary: "[17:34:05] Commands Xyz finished"
    """
    #self._redirectOutputs(False)
    process_widget.running = False
    process_widget.disconnectProcess(command_group)
    try :
      self._running_groups.remove(command_group)
    except ValueError :
      pass
#      self.io.write(u'cannot remove group %s to _running_groups' % (
#        command_group.group_id), level=u'debug')
    if command_group in self._wait_box_dict :
      self._wait_box_dict[command_group].done(0)
      devnull = self._wait_box_dict.pop(command_group)
      del devnull

    lastcmd = command_group.cmds[-1]
    try: lastcmd.exec_time.stop()
    except IOError :
      pass
#      self.io.write(u'Stopwatch, %s cmd is not running' % lastcmd.alias,
#        level=u'debug')

    command_group.exec_time.stop()

    # *** change tab state
    rcode = command_group.return_code
    if rcode == 0:
      self.widget_process_handler.changeTabState(process_widget, u'success')
    else :
      self.widget_process_handler.changeTabState(process_widget, u'fail')

    # *** write end summary
#    writers = [process_widget.stdout, process_widget.stderr]
#
#    text, style, level = self.formatTextCmdExecTime(lastcmd)
#    self.io.write(text, style, level, writers=writers)
#
#    text, style, level = self.formatTextCmdGroupFinished(command_group)
#    self.io.write(text, style, level, writers=writers)
#    self.io.writeStatus(text, style, level)
#
#    text, style, level = self.formatTextCmdGroupExecTime(command_group)
#    self.io.write(text, style, level, writers=writers)
#
#    self.io.write(u'\n', writers=writers)

    # *** update StatusBar
    #TODO: màj status bar
#    self.status_bar.clearMessage()
#    text, style, level = self.formatTextCmdGroupFinished(command_group)
#    self.status_bar.showMessage(text)

    next_cg = None
    wait = None
    if len(self._command_group_queue) :
      # search candidates (command_group in queue with all required
      # command_group finished)
      for p in self._command_group_queue :
        if not p[0]._wait_list : # no proc to wait so ... start this one
          self._command_group_queue.remove(p)
          next_cg, wait = p
          # good candidate, stop search
          break
        else :
          # this proc is supposed to be a good candidate (suppose all proc are
          # finished), check if it's True
          next_cg = p[0]
          for cg_to_wait in next_cg._wait_list :
            if cg_to_wait.state == CommonRun.Finished :
              continue
            else :
              # No it's False, one or more required command_group are not
              # finished. Stop for this candidate, try next in queue
              next_cg = None
              wait = None
              break
          if next_cg :
            # if all proc to wait are finished ... start this one
            self._command_group_queue.remove(p)
            next_cg, wait = p
            # good candidate, stop search
            break

      if next_cg :
        self._run(next_cg, wait)

  def _showWaitBox (self, command_group, process_widget, wait):
    if command_group not in self._wait_box_dict and wait is True :
      wait_box = QMessageBox(QMessageBox.Information, u'Please wait',
        tr_(u'Waiting for %(ALIAS)s (%(ID)s)') % dict(ALIAS=command_group.alias,
            ID=command_group.group_id), QMessageBox.Cancel)
      self._wait_box_dict[command_group] = wait_box
      if wait_box.exec_() != 0:
        process_widget.cancelProcess()

  def _runCommand (self, command_group, process_widget, cmds, cmd, wait):
    text, style, level = self.formatTextCmdStarted(cmd)
    command_group.logger.write(text, style)
    self.logger.write(text, style)

    cmd.exec_time.start()

    if issubclass(type(cmd), ManualCommand) :
      # *** If TextCommand, write text
      if isinstance(cmd, TextCommand) :
        command_group.logger.write(text, style)
        self.logger.write(text, style)

      # *** Change command state, finish successfully
      cmd.STATE = CommonRun.Finished
      if cmd.RETURN_CODE is None :
        cmd.RETURN_CODE = 0
      command_group._progress[0] += 1
      # *** run next command
      self._runNextCommand(command_group, process_widget, cmds, wait)

    elif type(cmd) is ShellCommand :
      # *** Create ShellRun object with ShellRun.thread=QProcess
      cwd = cmd.working_directory
      if not cwd.isdir() :
        cwd.mkdir()
      qprocess = QProcess(process_widget)
      qprocess.setWorkingDirectory(unicode(cwd))
      cmd.thread = ShellRun(qprocess)
      # *** Connect QProcess outputs to process_widgets
      process_widget.connectProcess(command_group, cmd, self)
      self._process[qprocess] = (command_group, process_widget, cmds, wait)
      # *** Connect "finished" signal to runNextCommand
      qprocess.finished.connect(self._onQProcessExitStatus)
      qprocess.error.connect(self._onQProcessError)
      # *** Start process
      qprocess.start(cmd.shellUString())
      command_group._progress[0] += 1
      self._showWaitBox(command_group, process_widget, wait)
      # *** When finished, _runNextCommand is launched with next command

    elif type(cmd) is PythonCommand :
      # *** Create PythonRun object with ShellRun.thread = QThread
      qthread = Thread(cmd, process_widget)
      cmd.thread = PythonRun(qthread)
      # *** Connect "finished" signal to runNextCommand
      self._process[qthread] = (command_group, process_widget, cmds, wait)
      self.widget_process_handler.connect(qthread, SIGNAL('finished()'),
        self.runNextCommand)
      # *** Start thread
      t = time.localtime ()
      text = u'\n[%02d:%02d:%02d] %s:\n' % (t.tm_hour, t.tm_min, t.tm_sec,
                                          cmd.alias)
      writers = [process_widget.stdout, process_widget.stderr]
#      self.io.write(text, writers=writers)
      qthread.start()
      command_group._progress[0] += 1
      self._showWaitBox(command_group, process_widget, wait)
      # *** When finished, _runNextCommand is launched with next command
    else :
      raise NotImplementedError

  def _requireGroups(self, *args):
    for cg_to_wait in args :
      if cg_to_wait != CommonRun.Finished :
        self._showWaitBox(cg_to_wait, self.main_process, True)

  def _onQProcessError(self, error):
    sender = self.widget_process_handler.sender()
    try :
      cg, process_widget, cmds, wait = self._process.pop(sender)
    except KeyError :
      pass
    else :
      err, qerr = PROCESS_ERRORS[error]
      writers = [process_widget.stdout, process_widget.stderr]
#      self.io.write(u'\nProcess error: %s\n' % (qerr), style=u'warn',
#        writers=writers)
      self._runNextCommand(cg, process_widget, cmds, wait)

  def _onQProcessExitStatus (self):
    sender = self.widget_process_handler.sender()
    print 'sender', sender
    try :
      cg, process_widget, cmds, wait = self._process.pop(sender)
    except KeyError :
      pass
    else :
      self._runNextCommand(cg, process_widget, cmds, wait)

  def _onProcessInBackgroundChecked(self, state):
    self.wait = not state

  def _onMaxProcessChanged(self, value):
    self.max_process = value

